package com.talkyun.openx.server.codec;

import com.talkyun.openx.server.core.ServiceFactory;
import com.talkyun.openx.server.core.ServiceRequest;
import com.talkyun.openx.server.helper.UriParser;
import com.talkyun.utils.json.JSON;
import com.talkyun.utils.json.JSONObject;
import com.talkyun.utils.para.ParaNamer;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;

import static com.talkyun.utils.Reflector.getInterface;
import static com.talkyun.utils.Reflector.getMethod;

public class JsonCodec extends AbstractCodec {
    private static final String HEAD_KEY = "_openx_head";
    private ParaNamer namer = new ParaNamer();
    private ServiceFactory sf;

    public JsonCodec(ServiceFactory sf) {
        this.sf = sf;
    }

    public ServiceRequest decode(String uri, String data) {
        return decodeData(this.decodeUri(uri), data);
    }

    public ServiceRequest decode(String uri, String data, String session) {
        ServiceRequest sr = decodeData(this.decodeUri(uri), data);
        sr.setSession(session);
        return sr;
    }

    private ServiceRequest decodeUri(String uri) {
        ServiceRequest sr = new ServiceRequest(null);
        // ex: uri => /xxx/app/find
        sr.setMapping("/" + UriParser.getUriLastPart(uri, 1, 2));
        sr.setMethod(UriParser.getUriLastPart(uri, 0, 1));
        if (super.isBlank(sr.getMapping()) || super.isBlank(sr.getMethod())) {
            throw new RuntimeException("Url format error! " + uri);
        }
        return sr;
    }

    private ServiceRequest decodeData(ServiceRequest sr, String data) {
        String mapping = sr.getMapping();
        String methods = sr.getMethod();

        Object service = sf.getService(mapping);
        if (service == null) {
            throw new RuntimeException("Not found " + mapping);
        }

        JSONObject json = JSON.parseObject(super.isBlank(data) ? "{}" : data);
        int argNum = json.exists(HEAD_KEY) ? json.size() - 1 : json.size();
        Method method = getMethod(getInterface(service), methods, argNum);
        if (method == null) {
            throw new RuntimeException("Not found " + mapping + ":" + methods);
        }

        // decode head
        JSONObject head = json.getJSONObject(HEAD_KEY);
        if (head != null) {
            for (Map.Entry<String, Object> entry : head.toMap().entrySet()) {
                Object val = entry.getValue();
                sr.head(entry.getKey(), val == null ? null : val.toString());
            }
        }

        // decode args
        List<ParaNamer.Param> list = namer.getParaList(method);
        if (list == null || list.isEmpty()) {
            return sr;
        }

        for (ParaNamer.Param param : list) {
            String argName = param.getName();
            Object argJson = json.get(argName);
            Type argType = param.getType();
            if (argJson == null) {
                sr.getArgs().put(argName, null);
            } else {
                sr.getArgs().put(argName, this.doDecode(argType, argJson));
            }
        }
        return sr;
    }


    private Object doDecode(Type type, Object json) {
        if (type instanceof Class<?>) {
            Class<?> clazz = (Class<?>) type;
            if (clazz.isEnum()) {
                String str = json.toString().trim();
                // enum ordinal
                if (str.length() == 0 || Character.isDigit(str.charAt(0))) {
                    return JSON.toJavaObject(str, (Class<?>) type);
                }
                // enum name, fix input
                str = str.startsWith("\"") ? str : "\"" + str;
                str = str.endsWith("\"") ? str : str + "\"";
                return JSON.toJavaObject(str, (Class<?>) type);
            } else if (clazz == String.class) {
                return json.toString();
            } else if (clazz == Date.class) {
                if (json instanceof Number) {
                    return new Date(((Number) json).longValue());
                } else {
                    String date = json.toString().trim();
                    // "yyyy-MM-dd"
                    if (date.length() == "yyyy-MM-dd".length()) {
                        return parseDate(date, "yyyy-MM-dd");
                    } else {
                        // "yyyy-MM-dd HH:mm:ss"
                        return parseDate(date, "yyyy-MM-dd HH:mm:ss");
                    }
                }
            } else {
                return JSON.toJavaObject(json.toString(), (Class<?>) type);
            }
        }

        // generic type
        if (type instanceof ParameterizedType) {
            ParameterizedType pt = ((ParameterizedType) type);
            Class<?> clazz = (Class<?>) pt.getRawType();
            // Generic List<E>
            if (clazz == List.class) {
                Class<?> targetClazz = (Class<?>) pt.getActualTypeArguments()[0];
                return JSON.toJavaList(json.toString(), targetClazz);
            }
            throw new RuntimeException("Not support type " + type);
        } else {
            throw new RuntimeException("Not support type " + type);
        }
    }

    private Date parseDate(String src, String fmt) {
        try {
            return new SimpleDateFormat(fmt).parse(src);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }
}